package com.google.code.p.restrpc.spring;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ApplicationObjectSupport;
import org.springframework.util.ClassUtils;

import com.google.code.p.restrpc.spring.assmebler.RpcServiceInfoAssembler;
import com.google.code.p.restrpc.spring.assmebler.RpcServiceInfoMetadataAssembler;
import com.google.code.p.restrpc.spring.metadata.AnnotationRpcAttributeSource;
import com.google.code.p.restrpc.spring.metadata.RpcAttributeSource;
import com.google.code.p.restrpc.spring.metadata.RpcMetadata;
import com.google.code.p.restrpc.spring.naming.MetadataNamingStrategy;

/**
 * @author dz
 */
public class RpcServiceAutoExporter extends ApplicationObjectSupport implements
                                                                    BeanClassLoaderAware,
                                                                    InitializingBean {
    
    private static final Log logger = LogFactory.getLog(RpcServiceAutoExporter.class);
    
    /*
     * Whether to eagerly init candidate beans when autodetecting RpcService
     */
    private boolean allowEagerInit = false;
    
    /*
     * A set of bean names that should be excluded from autodetection
     */
    private Set<String> excludedBeans = new HashSet<String>();
    
    /*
     * 
     */
    private Object[] interceptors = new Object[] {};
    
    /*
     * 
     */
    private RpcServiceInfoAssembler assembler = new RpcServiceInfoMetadataAssembler();
    
    /*
     * 
     */
    private RpcAttributeSource attributeSource = new AnnotationRpcAttributeSource();
    
    /*
     * 
     */
    private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
    
    /*
     * 
     */
    private boolean detectServicesInAncestorContexts = false;
    
    public void setBeanClassLoader(ClassLoader classLoader) {
        this.beanClassLoader = classLoader;
    }
    
    /**
     * Return the ClassLoader that this accessor operates in, to be used for
     * deserializing and for generating proxies.
     */
    protected ClassLoader getBeanClassLoader() {
        return this.beanClassLoader;
    }
    
    /**
     * Override the thread context ClassLoader with the environment's bean
     * ClassLoader if necessary, i.e. if the bean ClassLoader is not equivalent
     * to the thread context ClassLoader already.
     * 
     * @return the original thread context ClassLoader, or <code>null</code> if
     *         not overridden
     */
    protected ClassLoader overrideThreadContextClassLoader() {
        return ClassUtils.overrideThreadContextClassLoader(getBeanClassLoader());
    }
    
    /**
     * Reset the original thread context ClassLoader if necessary.
     * 
     * @param original
     *            the original thread context ClassLoader, or <code>null</code>
     *            if not overridden (and hence nothing to reset)
     */
    protected void resetThreadContextClassLoader(ClassLoader original) {
        if (original != null) {
            Thread.currentThread().setContextClassLoader(original);
        }
    }
    
    /**
     * @return the interceptors
     */
    public Object[] getInterceptors() {
        return interceptors;
    }
    
    /**
     * @param interceptors
     *            the interceptors to set
     */
    public void setInterceptors(Object[] interceptors) {
        this.interceptors = interceptors;
    }
    
    /**
     * @param interceptors
     *            the interceptors to set
     */
    public void setInterceptor(Object interceptor) {
        this.interceptors = new Object[] { interceptor };
    }
    
    /**
     * Set the list of names for beans that should be excluded from
     * autodetection.
     */
    public void setExcludedBeans(String[] excludedBeans) {
        this.excludedBeans = (excludedBeans != null ? new HashSet(Arrays.asList(excludedBeans))
                                                   : null);
    }
    
    /**
     * Indicates whether or not a particular bean name is present in the
     * excluded beans list.
     */
    private boolean isExcluded(String beanName) {
        return (this.excludedBeans != null && this.excludedBeans.contains(beanName));
    }
    
    /**
     * @return the allowEagerInit
     */
    public boolean isAllowEagerInit() {
        return allowEagerInit;
    }

    /**
     * @param allowEagerInit the allowEagerInit to set
     */
    public void setAllowEagerInit(boolean allowEagerInit) {
        this.allowEagerInit = allowEagerInit;
    }

    /**
     * @return the excludedBeans
     */
    public Set<String> getExcludedBeans() {
        return excludedBeans;
    }

    /**
     * @param excludedBeans the excludedBeans to set
     */
    public void setExcludedBeans(Set<String> excludedBeans) {
        this.excludedBeans = excludedBeans;
    }

    /**
     * @return the attributeSource
     */
    public RpcAttributeSource getAttributeSource() {
        return attributeSource;
    }

    /**
     * @param attributeSource the attributeSource to set
     */
    public void setAttributeSource(RpcAttributeSource attributeSource) {
        this.attributeSource = attributeSource;
    }

    /**
     * @return the detectHandlersInAncestorContexts
     */
    public boolean isDetectServicesInAncestorContexts() {
        return detectServicesInAncestorContexts;
    }

    /**
     * @param detectHandlersInAncestorContexts the detectHandlersInAncestorContexts to set
     */
    public void setDetectServicesInAncestorContexts(boolean detectHandlersInAncestorContexts) {
        this.detectServicesInAncestorContexts = detectHandlersInAncestorContexts;
    }

    /**
     * 
     */
    public void afterPropertiesSet() throws Exception {
        detectRemoteService();
    }
    
    /**
     * Export all remote service found in the current ApplicationContext.
     */
    protected void detectRemoteService() throws BeansException {
        if (logger.isDebugEnabled()) {
            logger.debug("Looking for RemoteService in application context: " + getApplicationContext());
        }
        
        ApplicationContext applicationContext = getApplicationContext();
        String[] beanNames = (this.detectServicesInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(),
                                                                                                                          Object.class)
                                                                   : applicationContext.getBeanNamesForType(Object.class));
        
        for (String beanName : beanNames) {
            if (isExcluded(beanName)) {
                continue;
            }
            
            Object bean = applicationContext.getBean(beanName);
            Class<?> serviceClass = applicationContext.getType(beanName);
            RpcMetadata metadata = attributeSource.getRpcMetadata(serviceClass);
            if (null == metadata) {
                continue;
            }
            
            String serviceName = getServiceName(metadata.getServiceName(),
                                                beanName);
            Class[] serviceInterfaces = serviceClass.getInterfaces();
            RpcServiceExporter exporter = new RpcServiceExporter();
            exporter.setBeanClassLoader(getBeanClassLoader());
            exporter.setExporterName(serviceName);
            exporter.setServiceClass(serviceClass);
            Class[] interfaces = serviceClass.getInterfaces();
            if (interfaces == null || interfaces.length == 0) {
                throw new InvalidMetadataException("No interface found for the bean, can't be exported as remote service!");
            }
            else if (interfaces.length > 1) {
                logger.warn(interfaces + " found! Only the first one works!");
                exporter.setServiceInterface(serviceInterfaces[0]);
            }
            else {
                exporter.setServiceInterface(serviceInterfaces[0]);
            }
            exporter.setInterceptors(interceptors);
            exporter.setService(bean);
            exporter.afterPropertiesSet();
        }
    }
    
    protected String getServiceName(String serviceName, String beanName) {
        return StringUtils.isNotEmpty(serviceName) ? serviceName : beanName;
    }
}
